package com.renjianbo.project.common.util;

import java.util.*;

/**
 * @Author 高博文
 * @Date 2020/09/27 17:20
 */
public class GeoHashUtil {

    private static int hashLength = 12; //经纬度转化为geohash长度
    private static int latLength = 30; //纬度转化为二进制长度
    private static int lngLength = 30; //经度转化为二进制长度

    private static double minLat;//每格纬度的单位大小
    private static double minLng;//每个经度的倒下
    private static final char[] CHARS = {'0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',
            'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};

    //定义编码映射关系
    final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();

    //初始化编码映射内容
    static {
        int i = 0;
        for (char c : CHARS)
            lookup.put(c, i++);
    }

    /**
     * 将数字转化为geohash二进制字符串
     * @param value
     * @param min
     * @param max
     * @param length
     * @return
     */
    private static boolean[] getHashArray(double value, double min, double max, int length) {
        if (value < min || value > max) {
            return null;
        }
        if (length < 1) {
            return null;
        }
        boolean[] result = new boolean[length];
        for (int i = 0; i < length; i++) {
            double mid = (min + max) / 2.0;
            if (value > mid) {
                result[i] = true;
                min = mid;
            } else {
                result[i] = false;
                max = mid;
            }
        }
        return result;
    }

    /**
     * 获取坐标的geo二进制字符串
     * @param lat
     * @param lng
     * @return
     */
    private static boolean[] getGeoBinary(double lat, double lng) {
        boolean[] latArray = getHashArray(lat, -90, 90, latLength);
        boolean[] lngArray = getHashArray(lng, -180, 180, lngLength);
        return merge(latArray, lngArray);
    }

    /**
     * 将五位二进制转化为base32
     * @param base32
     * @return
     */
    private static char getBase32Char(boolean[] base32) {
        if (base32 == null || base32.length != 5) {
            return ' ';
        }
        int num = 0;
        for (boolean bool : base32) {
            num <<= 1;
            if (bool) {
                num += 1;
            }
        }
        return CHARS[num % CHARS.length];
    }

    /**
     * 根据二进制和范围解码
     *
     * @param bs
     * @param floor
     * @param ceiling
     * @return
     */
    private static double decode(BitSet bs, double floor, double ceiling) {
        double mid = 0;
        for (int i = 0; i < bs.length(); i++) {
            mid = (floor + ceiling) / 2;
            if (bs.get(i))
                floor = mid;
            else
                ceiling = mid;
        }
        return mid;
    }

    /**
     * 设置经纬度的最小单位
     */
    private static void setMinLatLng() {
        minLat = 180;
        for (int i = 0; i < latLength; i++) {
            minLat /= 2.0;
        }
        minLng = 360;
        for (int i = 0; i < lngLength; i++) {
            minLng /= 2.0;
        }
    }

    /**
     * 设置经纬度转化为geohash长度
     * @param length
     * @return
     */
    private static boolean sethashLength(int length) {
        setMinLatLng();
        if (length < 1) {
            return false;
        }
        hashLength = length;
        latLength = (length * 5) / 2;
        if (length % 2 == 0) {
            lngLength = latLength;
        } else {
            lngLength = latLength + 1;
        }
        setMinLatLng();
        return true;
    }

    /**
     * 根据距离设置base32长度
     *
     * @param radius
     * @return
     */
    private static int getEffectDigitNumber(int radius) {
        int result = 0;
        if (radius <= 0) result = 0;
        else if (radius < 1) result = 10;
        else if (radius < 5) result = 9;
        else if (radius < 20) result = 8;
        else if (radius < 77) result = 7;
        else if (radius < 610) result = 6;
        else if (radius < 2400) result = 5;
        else if (radius < 20000) result = 4;
        else if (radius < 78000) result = 3;
        else if (radius < 630000) result = 2;
        else result = 0;
        return result;
    }

    /**
     * @param lat    纬度
     * @param lng    经度
     * @param length base32长度
     * @return
     * @Author:lulei
     * @Description: 获取经纬度的base32字符串
     */
    public static String encode(double lat, double lng, int length) {
        sethashLength(length);
        setMinLatLng();
        boolean[] bools = getGeoBinary(lat, lng);
        if (bools == null) {
            return null;
        }
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < bools.length; i = i + 5) {
            boolean[] base32 = new boolean[5];
            for (int j = 0; j < 5; j++) {
                base32[j] = bools[i + j];
            }
            char cha = getBase32Char(base32);
            if (' ' == cha) {
                return null;
            }
            sb.append(cha);
        }
        return sb.toString();
    }

    /**
     * 对编码后的字符串解码
     *
     * @param geohash
     * @return
     */
    public static double[] decode(String geohash) {
        StringBuilder buffer = new StringBuilder();
        for (char c : geohash.toCharArray()) {

            int i = lookup.get(c) + 32;
            buffer.append(Integer.toString(i, 2).substring(1));
        }
        BitSet lonset = new BitSet();
        BitSet latset = new BitSet();
        //偶数位，经度
        int j = 0;
        for (int i = 0; i < 5 * 6 * 2; i += 2) {
            boolean isSet = false;
            if (i < buffer.length())
                isSet = buffer.charAt(i) == '1';
            lonset.set(j++, isSet);
        }
        //奇数位，纬度
        j = 0;
        for (int i = 1; i < 5 * 6 * 2; i += 2) {
            boolean isSet = false;
            if (i < buffer.length())
                isSet = buffer.charAt(i) == '1';
            latset.set(j++, isSet);
        }
        double lon = decode(lonset, -180, 180);
        double lat = decode(latset, -90, 90);
        return new double[]{lat, lon};
    }

    /**
     * @return
     * @Author:lulei
     * @Description: 求所在坐标点及周围点组成的九个
     */
    public static List<String> encodeFor9(double lat, double lng, int length) {
        setMinLatLng();
        double leftLat = lat - minLat;
        double rightLat = lat + minLat;
        double upLng = lng - minLng;
        double downLng = lng + minLng;
        List<String> base32For9 = new ArrayList<String>();
        //左侧从上到下 3个
        String leftUp1 = encode(leftLat, upLng, length);
        if (!(leftUp1 == null || "".equals(leftUp1))) {
            base32For9.add(leftUp1);
        }
        String leftMid2 = encode(leftLat, lng, length);
        if (!(leftMid2 == null || "".equals(leftMid2))) {
            base32For9.add(leftMid2);
        }
        String leftDown3 = encode(leftLat, downLng, length);
        if (!(leftDown3 == null || "".equals(leftDown3))) {
            base32For9.add(leftDown3);
        }
        //中间从上到下 3个
        String midUp4 = encode(lat, upLng, length);
        if (!(midUp4 == null || "".equals(midUp4))) {
            base32For9.add(midUp4);
        }
        String midMid5 = encode(lat, lng, length);
        if (!(midMid5 == null || "".equals(midMid5))) {
            base32For9.add(midMid5);
        }
        String midDown6 = encode(lat, downLng, length);
        if (!(midDown6 == null || "".equals(midDown6))) {
            base32For9.add(midDown6);
        }
        //右侧从上到下 3个
        String rightUp7 = encode(rightLat, upLng, length);
        if (!(rightUp7 == null || "".equals(rightUp7))) {
            base32For9.add(rightUp7);
        }
        String rightMid8 = encode(rightLat, lng, length);
        if (!(rightMid8 == null || "".equals(rightMid8))) {
            base32For9.add(rightMid8);
        }
        String rightDown9 = encode(rightLat, downLng, length);
        if (!(rightDown9 == null || "".equals(rightDown9))) {
            base32For9.add(rightDown9);
        }
        return base32For9;
    }

    /**
     * @param latArray
     * @param lngArray
     * @return
     * @Author:lulei
     * @Description: 合并经纬度二进制
     */
    public static boolean[] merge(boolean[] latArray, boolean[] lngArray) {
        if (latArray == null || lngArray == null) {
            return null;
        }
        boolean[] result = new boolean[lngArray.length + latArray.length];
        Arrays.fill(result, false);
        for (int i = 0; i < lngArray.length; i++) {
            result[2 * i] = lngArray[i];
        }
        for (int i = 0; i < latArray.length; i++) {
            result[2 * i + 1] = latArray[i];
        }
        return result;
    }

    /**
     * 根据距离返回坐标点周围(共9个)base32字符串
     *
     * @param lat
     * @param lng
     * @param radius 单位米
     * @return
     */
    public static List<String> nearbyEncode(double lat, double lng, int radius) {
        int number = getEffectDigitNumber(radius);
        List<String> strings = encodeFor9(lat, lng, number);
        return strings;
    }

    /**
     * 计算两个坐标点距离
     *
     * @param longitudeFrom
     * @param latitudeFrom
     * @param longitudeTo
     * @param latitudeTo
     * @return
     */
//    public static double getDistance(double longitudeFrom, double latitudeFrom, double longitudeTo, double latitudeTo) {
//        GlobalCoordinates source = new GlobalCoordinates(latitudeFrom, longitudeFrom);
//        GlobalCoordinates target = new GlobalCoordinates(latitudeTo, longitudeTo);
//        double distance = new GeodeticCalculator().calculateGeodeticCurve(Ellipsoid.Sphere, source, target).getEllipsoidalDistance();
//        return distance;
//    }


}
